﻿using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Remote_server
{
    /// <summary>
    /// Works similar to the System.Net.Sockets.TcpListener
    /// class, accepting RspClients asynchronously
    /// </summary>
    public class RspListener : IDisposable
    {
        /// <summary>
        /// This event gets raised when a new client has
        /// connected and entered a correct password
        /// </summary>
        public event RspClientAcceptedEventHandler ClientAccepted;

        /// <summary>
        /// List of available Commands, it is not recommended
        /// that this list is modified manually, use the
        /// AddCommand and RemoveCommand methods instead
        /// </summary>
        public List<string> Commands { get; set; }
        /// <summary>
        /// The unencrypted password used to authenticate
        /// a session
        /// </summary>
        public string Password { get; set; }
        /// <summary>
        /// The port which the listener is monitoring
        /// </summary>
        public ushort Port { get; private set; }

        //Underlying TcpListener
        private TcpListener listener;
        //This thread gets aborted when the listener is closed
        private Thread listeningThread = null;
        //A list of clients that have connected, Close() is
        //called on these when this listener is closed
        private List<RspClient> clients;

        /// <summary>
        /// Initialize this listener with specific password
        /// and listening port
        /// </summary>
        /// <param name="port">The port to monitor after starting</param>
        /// <param name="password">The authentication password used by
        /// connecting clients</param>
        public RspListener(ushort port, string password)
        {
            Port = port;
            Password = password;
            Commands = new List<string>();

            listener = new TcpListener(IPAddress.Any, (int)port);
            clients = new List<RspClient>();
        }

        /// <summary>
        /// Start the underlying TcpListener, this call
        /// is required before calling the AcceptRspClients method
        /// </summary>
        public void Start()
        {
            listener.Start();
        }

        /// <summary>
        /// Stop listening, this also closes all connections
        /// that this listener has started
        /// </summary>
        public void Stop()
        {
            if (listeningThread != null)
            {
                listeningThread.Abort();
                listeningThread.Join();
                listeningThread = null;
                foreach (RspClient rc in clients)
                    rc.Close();
                clients.Clear();
            }
        }

        /// <summary>
        /// Same as calling Stop()
        /// </summary>
        public void Dispose()
        {
            Stop();
        }

        /// <summary>
        /// Start a listening thread that accepts clients
        /// asynchronously, accepted clients are accessable
        /// using the ClientAccepted event
        /// </summary>
        public void AcceptRspClients()
        {
            Thread t = new Thread(delegate()
            {
                try
                {
                    while (true)
                    {
                        if (listener.Pending())
                        {
                            TcpClient tcp = listener.AcceptTcpClient();
                            if (Authenticate(tcp))
                                AcceptClient(new RspClient(tcp) { Commands = new List<string>(Commands) });
                            else
                                tcp.Close();
                        }
                        Thread.Sleep(2000);
                    }
                }
                catch (ThreadAbortException)
                {
                    listener.Stop();
                }
            });

            listeningThread = t;
            t.IsBackground = true;
            t.Start();
        }

        /// <summary>
        /// Remove all command strings associated to
        /// a specific command
        /// </summary>
        /// <param name="command">A value from the Commands
        /// enumeration specifying what type of command is
        /// being removed</param>
        public void RemoveCommand(Commands command)
        {
            string[] cmds = CommandStrings.S(command);
            Commands.RemoveAll(p => cmds.Contains(p));
        }

        /// <summary>
        /// Add all command strings associated to
        /// a specific command
        /// </summary>
        /// <param name="command">A value from the Commands
        /// enumeration specifying what type of command is
        /// being added</param>
        public void AddCommand(Commands command)
        {
            RemoveCommand(command);
            Commands.AddRange(CommandStrings.S(command));
        }

        /// <summary>
        /// Get the string that is sent to any clients upon
        /// succesful connection. This string is based on
        /// the commands added to this listener
        /// </summary>
        public string AcceptString
        {
            get
            {
                StringBuilder sb = new StringBuilder();
                foreach (string cmd in Commands)
                    sb.Append(sb.Length == 0 ? cmd : "," + cmd);

                return String.Format("OK {0}", sb);
            }
        }

        /// <summary>
        /// Try to authenticate a connecting client
        /// </summary>
        /// <param name="client">The connecting TcpClient</param>
        /// <returns>True if the client entered a well formed
        /// login command and a correct password, false otherwise</returns>
        private bool Authenticate(TcpClient client)
        {
            bool accepted = false;
            StreamReader sr = new StreamReader(client.GetStream());

            try     //The client might disconnect before writing anything
            {
                string ln = sr.ReadLine();
                if (ln.StartsWith("LOGIN "))
                {
                    string pass = ln.Substring("LOGIN ".Length);
                    accepted = pass == Password;
                }
                StreamWriter sw = new StreamWriter(client.GetStream());
                sw.WriteLine(accepted ? AcceptString : "NOTOK");
                sw.Flush();
            }
            catch { return false; }

            return accepted;
        }

        /// <summary>
        /// Accept a client, adding it to the list of connected
        /// clients and raising the ClientAccepted event
        /// </summary>
        /// <param name="client">The authenticated client</param>
        private void AcceptClient(RspClient client)
        {
            clients.Add(client);
            if (ClientAccepted != null)
                ClientAccepted(this, new RspClientAcceptedEventArgs(client));
        }
    }
}
